# 主要的作用：元素的bbox的相关内容（获取实际页面所需的bbox，算上矩阵变换），多个Element元素之间的关系判断
"""
边界框bbox表示与管理：通过fitz.Rect类型存储和操作元素的边界框，提供了边界框的创建、更新、合并等基本操作。

坐标系统转换：处理PyMuPDF中未旋转页面坐标系统与实际页面坐标系统之间的转换，自动应用旋转矩阵。

空间关系判断：提供了一系列判断元素之间空间关系的方法，如：
    contains() - 判断是否包含另一个元素
    get_main_bbox() - 获取两个元素交集超过阈值的并集
    vertically_align_with() - 判断是否垂直对齐
    horizontally_align_with() - 判断是否水平对齐
    in_same_row() - 判断是否在同一行

元素层次结构：通过parent属性维护元素间的层次关系。

调试支持：提供plot()方法在PDF页面中绘制边界框用于调试。
"""

"""Object with a bounding box, e.g. Block, Line, Span.

Based on ``PyMuPDF``, the coordinates (e.g. bbox of ``page.get_text('rawdict')``) are generally
provided relative to the un-rotated page; while this ``pdf2docx`` library works under real page
coordinate system, i.e. with rotation considered. So, any instances created by this Class are
always applied a rotation matrix automatically.

Therefore, the bbox parameter used to create ``Element`` instance MUST be relative to un-rotated
CS. If final coordinates are provided, should update it after creating an empty object::

    Element().update_bbox(final_bbox)

.. note::
    An exception is ``page.get_drawings()``, the coordinates are converted to real page CS already.
"""

"""具有边界框的对象，例如 Block（块）、Line（行）、Span（跨度）。

基于 ``PyMuPDF``，坐标（例如 ``page.get_text('rawdict')`` 的 bbox）通常是相对于未旋转页面提供的；
而这个 ``pdf2docx`` 库在实际页面坐标系统下工作，即考虑了旋转。因此，由这个类创建的任何实例
都会自动应用旋转矩阵。

因此，用于创建 ``Element`` 实例的 bbox 参数必须相对于未旋转的坐标系统。如果提供了最终坐标，
应该在创建空对象后更新它::

    Element().update_bbox(final_bbox)

.. note::
    ``page.get_drawings()`` 是一个例外，其坐标已经转换为实际页面坐标系统。
"""

import copy
import fitz
from .share import IText
from . import constants


class Element(IText):
    """Boundary box with attribute in fitz.Rect type."""

    """具有 fitz.Rect 类型属性的边界框。"""

    # all coordinates are related to un-rotated page in PyMuPDF
    # e.g. Matrix(0.0, 1.0, -1.0, 0.0, 842.0, 0.0)
    # 所有坐标都与 PyMuPDF 中的未旋转页面相关
    # 例如 Matrix(0.0, 1.0, -1.0, 0.0, 842.0, 0.0)
    ROTATION_MATRIX = fitz.Matrix(0.0)  # rotation angle = 0 degree by default
    # 默认旋转角度为0度

    # @classmethod是类方法装饰器，使用该装饰器，第一个参数是cls，代表类本身
    # 调用方式和其他类方法一致，为 类.类方法，eg:Element.set_rotation_matrix
    @classmethod
    def set_rotation_matrix(cls, rotation_matrix):
        """Set global rotation matrix.

        Args:
            Rotation_matrix (fitz.Matrix): target matrix
        """
        """设置全局！！旋转矩阵，因为是类属性，所以是所有Element实例共享的，意味着全局。

        参数：
            Rotation_matrix (fitz.Matrix): 目标矩阵
        """
        if rotation_matrix and isinstance(rotation_matrix, fitz.Matrix):
            cls.ROTATION_MATRIX = rotation_matrix

    @classmethod
    def pure_rotation_matrix(cls):
        """Pure rotation matrix used for calculating text direction after rotation."""
        """用于计算旋转后文本方向的纯旋转矩阵。去除了e,f，因为旋转矩阵中e,f是偏移量，与旋转无关。"""
        a, b, c, d, e, f = cls.ROTATION_MATRIX
        return fitz.Matrix(a, b, c, d, 0, 0)

    def __init__(self, raw: dict = None, parent=None):
        """Initialize Element and convert to the real (rotation considered) page CS."""
        """ 初始化 Element 并转换到实际的（考虑旋转的）页面坐标系统。"""
        self.bbox = fitz.Rect()  # type: fitz.Rect
        self._parent = parent  # type: Element

        # NOTE: Any coordinates provided in raw is in original page CS
        # (without considering page rotation).
        # 注意：raw 中提供的任何坐标都在原始页面坐标系统中
        # （不考虑页面旋转）
        if "bbox" in (raw or {}):
            # 从raw中获取原始的bbox，然后乘以矩阵，获取最终的bbox
            # PyMuPDF重载了*运算符，使fitz.Rect对象可以直接与fitz.Matrix对象相乘
            rect = fitz.Rect(raw["bbox"]) * Element.ROTATION_MATRIX
            self.update_bbox(rect)

    def __bool__(self):
        # __bool__ 方法定义了一个对象在布尔上下文中的行为，即当使用 if obj, while obj, bool(obj) 等场合时，如何判断该对象是真还是假。
        """Real object when bbox is defined."""
        """当 bbox 被定义时为真实对象。"""
        # NOTE inconsistent results of fitz.Rect for different version of pymupdf, e.g.,
        # a = fitz.Rect(3,3,2,2)
        #                   bool(a)      a.get_area()       a.is_empty
        # pymupdf 1.23.5      True            1.0              True
        # pymupdf 1.23.8      True            0.0              True
        # bool(fitz.Rect())==False
        # NOTE: do not use `return not self.bbox.is_empty` here
        # 注意：不同版本的 pymupdf 对 fitz.Rect 的结果不一致，例如：
        # a = fitz.Rect(3,3,2,2)
        #                   bool(a)      a.get_area()       a.is_empty
        # pymupdf 1.23.5      True            1.0              True
        # pymupdf 1.23.8      True            0.0              True
        # bool(fitz.Rect())==False
        # 注意：这里不要使用 `return not self.bbox.is_empty`
        return bool(self.bbox)

    def __repr__(self):
        """
        例如，如果有一个Element对象，其bbox属性是fitz.Rect(10, 20, 30, 40)，那么调用repr()函数或者在交互式环境中输入这个对象时，会显示类似：Element((10.0, 20.0, 30.0, 40.0))
        与__str__方法相比，__repr__更加面向开发者，提供更加详细和无歧义的信息，而__str__则更加面向用户，可能会提供更加友好但可能信息量较少的字符串表示。
        在这个类中，没有定义__str__方法，所以当使用print()函数或str()函数时，Python会默认调用__repr__方法。
        """
        return f"{self.__class__.__name__}({tuple(self.bbox)})"

    # ------------------------------------------------
    # parent element
    # 父元素
    # ------------------------------------------------
    @property
    def parent(self):
        return self._parent

    @parent.setter
    def parent(self, parent):
        self._parent = parent

    # ------------------------------------------------
    # bbox operations
    # bbox 操作
    # ------------------------------------------------
    def copy(self):
        """make a deep copy."""
        """创建深度拷贝。"""
        # NOTE: can't serialize data because parent is an Object,
        # so set it None in advance.
        # 注意：因为父对象是一个Object，无法序列化数据，
        # 所以提前将其设置为None。
        parent, self.parent = self._parent, None
        obj = copy.deepcopy(self)
        self._parent = parent  # set back parent
        # 重新设置父对象
        return obj

    def get_expand_bbox(self, dt: float):
        """Get expanded bbox with margin in both x- and y- direction.

        Args:
            dt (float): Expanding margin.

        Returns:
            fitz.Rect: Expanded bbox.

        .. note::
            This method creates a new bbox, rather than changing the bbox of itself.
        """
        """获取在x和y方向上都有边距扩展的bbox。

        参数：
            dt (float): 扩展边距。

        返回：
            fitz.Rect: 扩展后的bbox。
            
        作用：
            容错处理：在处理文档元素时，给边界框增加一定的边距可以更好地处理元素之间的关系，特别是在判断元素是否相交、相邻或包含时。
            视觉间距：在布局分析或渲染时，为元素提供一定的"呼吸空间"，避免元素之间过于紧凑。
            包含关系判断：当处理元素的层级关系时，稍微扩大边界可以更准确地判断包含关系。
            边界模糊处理：PDF解析中，有时元素的实际边界可能不够精确，扩展边界可以弥补这种不精确性。

        .. note::
            此方法创建一个新的bbox，而不是改变自身的bbox!!!!!
        """
        # self.bbox + (-dt, -dt, dt, dt) 这种写法是通过 PyMuPDF 库中的 fitz.Rect 类重载了加法运算符（__add__方法）实现的。
        return self.bbox + (-dt, -dt, dt, dt)

    def update_bbox(self, rect):
        """Update current bbox to specified ``rect``.

        Args:
            rect (fitz.Rect or list): bbox-like ``(x0, y0, x1, y1)``,
                in real page CS (with rotation considered).
        """
        """更新当前bbox为指定的 ``rect``。

        参数：
            rect (fitz.Rect or list): 类似bbox的 ``(x0, y0, x1, y1)``，
                在实际页面坐标系统中（考虑旋转）。
        """
        # 将rect转换为fitz.Rect类型，并四舍五入到小数点后一位
        self.bbox = fitz.Rect([round(x, 1) for x in rect])
        return self

    def union_bbox(self, e):
        """Update current bbox to the union with specified Element.

        Args:
            e (Element): The target to get union

        Returns:
            Element: self
        """
        """更新当前bbox为与指定Element的并集。
        也就是将两个Element实例的bbox合并，获取它们的bbox的并集。

        参数：
            e (Element): 要进行并集运算的目标

        返回：
            Element: self
        """
        return self.update_bbox(self.bbox | e.bbox)

    # --------------------------------------------
    # location relationship to other Element instance
    # 与其他Element实例的位置关系
    # --------------------------------------------
    def contains(self, e: "Element", threshold: float = 1.0):
        """Whether given element is contained in this instance, with margin considered.

        Args:
            e (Element): Target element
            threshold (float, optional): Intersection rate.
                Defaults to 1.0. The larger, the stricter.

        Returns:
            bool: [description]
        """
        """判断给定元素是否包含在此实例中，考虑边距。

        参数：
            e (Element): 目标元素
            threshold (float, optional): 交叉率。
                默认为1.0。值越大，判断越严格。

        返回：
            bool: [描述]
        """
        S = e.bbox.get_area()
        if not S:
            return False

        # it's not practical to set a general threshold to consider the margin, so two steps:
        # - set a coarse but acceptable area threshold,
        # - check the length in main direction strictly
        # A contains B => A & B = B
        # 设置一个通用的阈值来考虑边距是不实际的，所以分两步：
        # - 设置一个粗略但可接受的面积阈值
        # - 严格检查主方向的长度
        # A包含B => A & B = B
        intersection = self.bbox & e.bbox
        factor = round(intersection.get_area() / S, 2)
        if factor < threshold:
            return False

        # check length
        # 检查长度
        if self.bbox.width >= self.bbox.height:
            return self.bbox.width + constants.MINOR_DIST >= e.bbox.width
        return self.bbox.height + constants.MINOR_DIST >= e.bbox.height

    def get_main_bbox(self, e, threshold: float = 0.95):
        """If the intersection with ``e`` exceeds the threshold, return the union of
        these two elements; else return None.

        Args:
            e (Element): Target element.
            threshold (float, optional): Intersection rate. Defaults to 0.95.

        Returns:
            fitz.Rect: Union bbox or None.
        """
        """如果与 ``e`` 的交集超过阈值，返回这两个元素的并集；
        否则返回None。

        参数：
            e (Element): 目标元素。
            threshold (float, optional): 交叉率。默认为0.95。

        返回：
            fitz.Rect: 并集bbox或None。
        """
        bbox_1 = self.bbox
        bbox_2 = e.bbox if hasattr(e, "bbox") else fitz.Rect(e)

        # areas
        # 面积
        b = bbox_1 & bbox_2
        if b.is_empty:
            return None  # no intersection
        # 没有交集

        # Note: if bbox_1 and bbox_2 intersects with only an edge, b is not empty but b.get_area()=0
        # so give a small value when they're intersected but the area is zero
        # 注意：如果bbox_1和bbox_2仅在边缘相交，b不为空但b.get_area()=0
        # 所以当它们相交但面积为零时，给一个小的值
        a1, a2, a = bbox_1.get_area(), bbox_2.get_area(), b.get_area()
        factor = a / min(a1, a2) if a else 1e-6
        return bbox_1 | bbox_2 if factor >= threshold else None

    def vertically_align_with(
        self, e, factor: float = 0.0, text_direction: bool = True
    ):
        """Check whether two Element instances have enough intersection in vertical direction,
        i.e. perpendicular to reading direction.

        Args:
            e (Element): Object to check with
            factor (float, optional): Threshold of overlap ratio, the larger it is, the higher
                probability the two bbox-es are aligned.
            text_direction (bool, optional): Consider text direction or not. True by default.

        Returns:
            bool: [description]

        Examples::

            +--------------+
            |              |
            +--------------+
                    L1
                    +-------------------+
                    |                   |
                    +-------------------+
                            L2

        An enough intersection is defined based on the minimum width of two boxes::

            L1+L2-L>factor*min(L1,L2)
        """
        """检查两个Element实例在垂直方向上是否有足够的交集，
        即垂直于阅读方向。

        参数：
            e (Element): 要检查的对象
            factor (float, optional): 重叠率阈值，值越大，
                两个bbox对齐的概率越高。
            text_direction (bool, optional): 是否考虑文本方向。默认为True。

        返回：
            bool: [描述]

        示例::

            +--------------+
            |              |
            +--------------+
                    L1
                    +-------------------+
                    |                   |
                    +-------------------+
                            L2

        足够的交集基于两个框的最小宽度定义::

            L1+L2-L>factor*min(L1,L2)
        """
        if not e or not bool(self):
            return False

        # text direction
        # 文本方向
        idx = 1 if text_direction and self.is_vertical_text else 0

        L1 = self.bbox[idx + 2] - self.bbox[idx]
        L2 = e.bbox[idx + 2] - e.bbox[idx]
        L = max(self.bbox[idx + 2], e.bbox[idx + 2]) - min(self.bbox[idx], e.bbox[idx])

        eps = 1e-3  # tolerant
        # 容差
        return L1 + L2 - L + eps >= factor * min(L1, L2)

    def horizontally_align_with(
        self, e, factor: float = 0.0, text_direction: bool = True
    ):
        """Check whether two Element instances have enough intersection in horizontal direction,
        i.e. along the reading direction.

        Args:
            e (Element): Element to check with
            factor (float, optional): threshold of overlap ratio, the larger it is, the higher
                probability the two bbox-es are aligned.
            text_direction (bool, optional): consider text direction or not. True by default.

        Examples::

            +--------------+
            |              | L1  +--------------------+
            +--------------+     |                    | L2
                                 +--------------------+

        An enough intersection is defined based on the minimum width of two boxes::

            L1+L2-L>factor*min(L1,L2)
        """
        """检查两个Element实例在水平方向上是否有足够的交集，
        即沿着阅读方向。

        参数：
            e (Element): 要检查的元素
            factor (float, optional): 重叠率阈值，值越大，
                两个bbox对齐的概率越高。
            text_direction (bool, optional): 是否考虑文本方向。默认为True。

        示例::

            +--------------+
            |              | L1  +--------------------+
            +--------------+     |                    | L2
                                 +--------------------+

        足够的交集基于两个框的最小宽度定义::

            L1+L2-L>factor*min(L1,L2)
        """
        if not e or not bool(self):
            return False

        # text direction
        # 文本方向
        idx = 0 if text_direction and self.is_vertical_text else 1

        L1 = self.bbox[idx + 2] - self.bbox[idx]
        L2 = e.bbox[idx + 2] - e.bbox[idx]
        L = max(self.bbox[idx + 2], e.bbox[idx + 2]) - min(self.bbox[idx], e.bbox[idx])

        eps = 1e-3  # tolerant
        # 容差
        return L1 + L2 - L + eps >= factor * min(L1, L2)

    def in_same_row(self, e):
        """Check whether in same row/line with specified Element instance.
        With text direction considered.

           Taking horizontal text as an example:

           * yes: the bottom edge of each box is lower than the centerline of the other one;
           * otherwise, not in same row.

        Args:
            e (Element): Target object.

        .. note::
            The difference to method ``horizontally_align_with``: they may not in same line, though
            aligned horizontally.
        """
        """检查是否与指定的Element实例在同一行/线上。
        考虑文本方向。

           以水平文本为例：

           * 是：每个框的底边都低于另一个框的中心线；
           * 否则，不在同一行。

        参数：
            e (Element): 目标对象。

        .. note::
            与 ``horizontally_align_with`` 方法的区别：它们可能不在同一行，
            尽管水平对齐。
        """
        if not e or self.is_horizontal_text != e.is_horizontal_text:
            return False

        # normal reading direction by default
        # 默认为正常阅读方向
        idx = 1 if self.is_horizontal_text else 0

        c1 = (self.bbox[idx] + self.bbox[idx + 2]) / 2.0
        c2 = (e.bbox[idx] + e.bbox[idx + 2]) / 2.0
        res = (
            c1 <= e.bbox[idx + 2] and c2 <= self.bbox[idx + 2]
        )  # Note y direction under PyMuPDF context
        # 注意PyMuPDF上下文中的y方向
        return res

    # ------------------------------------------------
    # others
    # 其他
    # ------------------------------------------------
    def store(self):
        """Store properties in raw dict."""
        """将属性存储在原始字典中。"""
        return {"bbox": tuple(x for x in self.bbox)}

    def plot(
        self,
        page,
        stroke: tuple = (0, 0, 0),
        width: float = 0.5,
        fill: tuple = None,
        dashes: str = None,
    ):
        """Plot bbox in PDF page for debug purpose."""
        """在PDF页面中绘制bbox，用于调试目的。"""
        page.draw_rect(
            self.bbox,
            color=stroke,
            fill=fill,
            width=width,
            dashes=dashes,
            overlay=False,
            fill_opacity=0.5,
        )
